iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0
Software Development

Java工程師的報表入門與實作系列 第 15

JasperReports-SubReport 子報表(下)

  • 分享至 

  • xImage
  •  

上一篇已經大致完成子報表模板的設置,今天來看看後端要怎麼處理資料。

Java後端處理DataSource

這次的情境是每個科系的各個課程成績平均,科系作為主報表的資料,各個課程以及成績平均作為子報表的資料,是一個對多個,我個人會處理為主報表List中再放入子報表List,可以明確知道這個科系包含哪些課程成績資料。

DB關聯圖

Dao查詢資料

SQL查詢資料映射到Dto中。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class StudentCourseScoreDto {
    private Integer studentId;

    private String firstName;

    private String lastName;

    private String gender;

    private String grade;

    private Integer departmentId;

    private String departmentName;

    private String departmentDesc;

    private Integer courseId;

    private String courseName;

    private String courseDesc;

    private String credit;

    private Integer scoreId;

    private Integer score;

    private Date testDate;
}
@Override
public List<StudentCourseScoreDto> getStudentCourseScoreData() {
    QStudentEntity qStudent = QStudentEntity.studentEntity;
    QDepartmentEntity qDepartment = QDepartmentEntity.departmentEntity;
    QCourseEntity qCourse = QCourseEntity.courseEntity;
    QScoreEntity qScore = QScoreEntity.scoreEntity;

    return queryFactory.select(Projections
                .bean(StudentCourseScoreDto.class,
                qStudent.studentId, qStudent.firstName, qStudent.lastName,
                qStudent.gender, qStudent.grade, qDepartment.departmentId,
                qDepartment.departmentName, qDepartment.departmentDesc,
                qCourse.courseId, qCourse.courseName, qCourse.courseDesc, 
                qCourse.credit, qScore.scoreId, qScore.score, qScore.testDate))
            .from(qStudent)
            .innerJoin(qDepartment)
                .on(qStudent.departmentId.eq(qDepartment.departmentId))
            .innerJoin(qScore)
                .on(qStudent.studentId.eq(qScore.studentId))
            .innerJoin(qCourse)
                .on(qScore.courseId.eq(qCourse.courseId))
            .fetch();
}

Service處理資料邏輯

  1. 先查詢資料,因為這次資料沒有很複雜,可以一次查出後續再整理。如果資料複雜或來源很不同,可能會分多次查詢出多個List再處理。
List<StudentCourseScoreDto> studentCourseScoreDtoList = 
            Optional.of(jasperReportDemoDao.getStudentCourseScoreData())
                    .orElse(new ArrayList<>());
  1. 以科系(departmentId)將資料分類
Map<Integer, List<StudentCourseScoreDto>> studentCourseScoreDtoMap = 
        studentCourseScoreDtoList.stream()
                .collect(Collectors.groupingBy(StudentCourseScoreDto::getDepartmentId));
  1. 主報表、子報表資料
    主報表對應子報表的物件為DepartmentCourseScoreAverageReportModel,裡面有主報表要顯示的科系中文名稱departmentDesc,還有用來對應子報表資料的departmentId,上一篇中有提到REPORT_PARAMETERS_MAP這個預設的參數型別固定是Map<String, Object>,以departmentId作為key的話要用String型別
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DepartmentCourseScoreAverageReportModel {
    private String departmentId;

    private String departmentDesc;

    // 子報表
    private List<SubReportAverageScoreModel> subReportAverageScoreModelList;
}

子報表資料放入課程名稱中文courseDesc與該課程平均成績averageScore。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SubReportAverageScoreModel {
    private String courseDesc;

    private BigDecimal averageScore;
}
  1. 整理Map<Integer, List> 為 主報表、子報表資料List
    在遍歷Map<Integer, List>的過程中,再以courseId來分類,就可以取得同課程的成績資料,計算成績平均後放入子報表物件
// 主報表、子報表物件清單
List<DepartmentCourseScoreAverageReportModel> mainReportModelList = new ArrayList<>();

for (Map.Entry<Integer, List<StudentCourseScoreDto>> entry : studentCourseScoreDtoMap.entrySet()) {
    Integer departmentId = entry.getKey();
    List<StudentCourseScoreDto> entryValue = entry.getValue();
    String departmentDesc = entryValue.get(0).getDepartmentDesc();

    // 用課程分群
    Map<Integer, List<StudentCourseScoreDto>> courseScoreDtoMap = entryValue.stream()
            .collect(Collectors.groupingBy(StudentCourseScoreDto::getCourseId));

    // 子報表清單
    List<SubReportAverageScoreModel> subReportModelList = new ArrayList<>();
    // 計算課程平均
    for (Map.Entry<Integer, List<StudentCourseScoreDto>> courseEntry : courseScoreDtoMap.entrySet()) {
        List<StudentCourseScoreDto> courseValue = courseEntry.getValue();
        String courseDesc = courseValue.get(0).getCourseDesc();
        BigDecimal totalScore = new BigDecimal(courseValue.stream().mapToInt(StudentCourseScoreDto::getScore).sum());
        BigDecimal averageScore = totalScore.divide(new BigDecimal(courseValue.size()), 2, RoundingMode.HALF_UP);

        // 課程名稱、平均成績放入子報表物件
        SubReportAverageScoreModel subReportAverageScoreModel = new SubReportAverageScoreModel(courseDesc, averageScore);
        subReportModelList.add(subReportAverageScoreModel);
    }

    // 主報表物件
    DepartmentCourseScoreAverageReportModel mainReportModel = new DepartmentCourseScoreAverageReportModel();
    // 為了作為parametersMap的key,型別須轉為String
    mainReportModel.setDepartmentId(departmentId.toString());
    mainReportModel.setDepartmentDesc(departmentDesc);
    mainReportModel.setSubReportAverageScoreModelList(subReportModelList);
    mainReportModelList.add(mainReportModel);
}

Facade匯出報表流程

  1. 查詢service整理好的資料
List<DepartmentCourseScoreAverageReportModel> mainReportModelList = 
                        reportDemoService.getDepartmentAverageScore();
  1. 設定報表參數
Map<String, Object> parametersMap = this.getDateParameters();

private Map<String, Object> getDateParameters() {
Map<String, Object> parametersMap = new HashMap<>();
        LocalDate localDate = new Date().toInstant()
                .atZone(ZoneId.systemDefault()).toLocalDate();
        parametersMap.put("date", localDate
                .format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
}

子報表

子報表的部分只要先執行生命週期中的「將jrxml模板編譯成jasper文件」、「將Java集合資料來源與Jasper報表進行綁定」這兩步驟,之後再與主報表一起之行後續步驟。

  1. 編譯子報表模板
    先將子報表模板編譯,在上一篇中我已經將我已經將「SubReport」的Expression屬性設為P${compiledSubReport},在這個步驟中可以把編譯後的子報表jasper文件放入REPORT_PARAMETERS_MAP。
    ▲ 如果使用手動編譯,路徑就直接以"xxx.jasper"放入主報表parametersMap中就好。
// 3. 設定子報表jasper文件
String subReportPath = "/Report/Jasper/DepartmentCourseScoreAverageSubReport.jrxml";
String expressionKey = "compiledSubReport";
try {
    ExportReportUtil.setSubReportJasper(parametersMap, subReportPath, expressionKey);
} catch (Exception e) {
    throw new RuntimeException(e);
}
public static void setSubReportJasper(Map<String, Object> parametersMap, String subReportExp, String expressionKey) throws Exception {
    // jasper文件 子報表
    try {
        JasperReport subReport = JasperCompileManager
            .compileReport(ExportReportUtil.class.getResourceAsStream(subReportExp));
        // 將子報表放入主報表參數
        parametersMap.put(expressionKey, subReport);
    } catch (Exception e) {
        throw new Exception();
    }
}
  1. 子報表dataSource對應主報表
    在上一篇中我已經將「SubReport」的Data Source Expression屬性的表達式設為$P{REPORT_PARAMETERS_MAP}.get($F{departmentId}),因此這個步驟必須將departmentId與對應的子報表DataSource放入parametersMap中,必須放入JRBeanCollectionDataSource,否則之後沒辦法直接將資料填入報表模板中
this.mapSubReportData(mainReportModelList, parametersMap);

private void mapSubReportData(List<DepartmentCourseScoreAverageReportModel> mainReportModelList, Map<String, Object> parametersMap) {
    for (DepartmentCourseScoreAverageReportModel mainReportModel : mainReportModelList) {
        String departmentId = mainReportModel.getDepartmentId();
        List<SubReportAverageScoreModel> subReportModelList = 
                    mainReportModel.getSubReportAverageScoreModelList();

        // 設置為子報表的資料來源
        JRDataSource subReportDataSource = 
                    new JRBeanCollectionDataSource(subReportModelList);
        parametersMap.put(departmentId, subReportDataSource);
    }
}

主報表

主報表的部分只要用同樣的方法完成報表生命週期就好了。

// 5.匯出excel byte[]
byte[] bytes = null;
try {
    String reportPath = "/Report/Jasper/DepartmentCourseScoreAverageReport.jrxml";
    bytes = ExportReportUtil
            .templateToExcelByte(mainReportModelList, reportPath, parametersMap);
} catch (Exception e) {
    throw new RuntimeException(e);
}
public static byte[] templateToExcelByte(List dataSourceList, String reportPath, Map<String, Object> parametersMap) throws Exception {

    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
        // 以JasperCompileManager將jrxml模板編譯成jasper文件
        JasperReport jasperReport = JasperCompileManager
            .compileReport(ExportReportUtil.class.getResourceAsStream(reportPath));

        // 將Java集合資料來源與Jasper報表進行綁定
        JRDataSource dataSource = new JRBeanCollectionDataSource(dataSourceList);

        // 將資料填入報表
        JasperPrint print = JasperFillManager
            .fillReport(jasperReport, parametersMap, dataSource);

        // 匯出
        SimpleXlsxReportConfiguration xlsxReportConfiguration = 
                                        new SimpleXlsxReportConfiguration();
        // setDetectCellType使excel偵測這個值的型別並轉換為對應的格式
        xlsxReportConfiguration.setDetectCellType(true);

        JRXlsxExporter exporter = new JRXlsxExporter();
        exporter.setConfiguration(xlsxReportConfiguration);
        exporter.setExporterInput(new SimpleExporterInput(print));
        exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(byteArrayOutputStream));
        exporter.exportReport();

        return byteArrayOutputStream.toByteArray();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

之後再將報表byte[]和檔案名稱處理就能生成excel檔了。

科系課程平均成績表Excel檔

匯出之後可以發現資料已經照著一開始所想的格式排列,主報表的科系對應到子報表的課程與成績平均。只差最後一步了,要將主報表的科系欄位合併儲存格

合併儲存格

合併儲存格的方法其實只要讓主報表欄位拓展到與對應子報表相同高度就好。

  • 選取主報表的所有欄位(此範例只有departmentDesc一個欄位)Text Field -> 右下區域屬性Stretch Type -> 選 「Relative To Band Height」或「ElementGroupHeight」 就完成啦
    • 「Relative To Band Height」在較新的Jaspersoft Studio版本中被標註為棄用,可以用「ElementGroupHeight」達到同樣效果


Reference


上一篇
JasperReports-SubReport 子報表(上)
系列文
Java工程師的報表入門與實作15
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言